﻿/*******************************************************************************
 * AAEngine
 * Copyright (c) 2010 Mike Jarosch
 *
 * Original source PushButton Engine:
 * Copyright (C) 2009 PushButton Labs, LLC
 * For more information see http://www.pushbuttonengine.com
 *
 * This file is licensed under the terms of the MIT license, which is included
 * in the "MIT License.txt" file at the root directory of this SDK.
 ******************************************************************************/

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using AAEngine.Engine.Debug;

namespace AAEngine.Engine.Core
{
    /// <summary>
    /// The process manager manages all time related functionality in the engine.     
    /// It provides mechanisms for performing actions every frame, every tick, or     
    /// at a specific time in the future.
    /// </summary>
    /// <remarks>
    /// A tick happens at a set interval defined by the TICKS_PER_SECOND constant.     
    /// Using ticks for various tasks that need to happen repeatedly instead of     
    /// performing those tasks every frame results in much more consistent output.     
    /// However, for animation related tasks, frame events should be used so the     
    /// display remains smooth.
    /// </remarks>
    /// <seealso cref="ITickedObject"/>
    /// <seealso cref="IAnimatedObject"/>
    public sealed class ProcessManager : DrawableGameComponent
    {
        class ProcessObject
        {
            public string ProfilerKey = null;
            public object Listener = null;
            public float Priority = 0.0f;
        }

        public ProcessManager(Game game) : base(game) { }

        public override void Update(GameTime gameTime)
        {
            Update((float)gameTime.TotalGameTime.TotalMilliseconds);

            base.Update(gameTime);
        }

        public override void Draw(GameTime gameTime)
        {
            Update((float)gameTime.TotalGameTime.TotalMilliseconds);

            base.Draw(gameTime);
        }

        /// <summary>
        /// If true, disables warnings about losing ticks.
        /// </summary>
        public static bool DisableSlowWarning 
        { 
            get { return _disableSlowWarning; } 
            set { _disableSlowWarning = value; }
        }
        private static bool _disableSlowWarning = true;

        /// <summary>
        /// The number of ticks that will happen every second.
        /// </summary>
        public static readonly int TICKS_PER_SECOND = 30;

        /// <summary>
        /// The rate at which ticks are fired, in seconds.
        /// </summary>
        public static readonly float TICK_RATE = 1.0f / TICKS_PER_SECOND;

        /// <summary>
        /// The rate at which ticks are fired, in milliseconds.
        /// </summary>
        public static readonly float TICK_RATE_MS = TICK_RATE * 1000;

        /// <summary>
        /// The maximum number of ticks that can be processed in a frame.
        /// </summary>
        /// <remarks>
        /// In some cases, a single frame can take an extremely long amount of         
        /// time. If several ticks then need to be processed, a game can         
        /// quickly get in a state where it has so many ticks to process         
        /// it can never catch up. This is known as a death spiral.
        /// 
        /// To prevent this we have a safety limit. Time is dropped so the         
        /// system can catch up in extraordinary cases. If your game is just         
        /// slow, then you will see that the ProcessManager can never catch up         
        /// and you will constantly get the "too many ticks per frame" warning,         
        /// if you have disableSlowWarning set to false.
        /// </remarks>
        public static readonly int MAX_TICKS_PER_FRAME = 10;

        List<Action> _deferredMethodQueue = new List<Action>();
        bool _started = false;
        float _virtualTime = 0.0f;
        float _interpolationFactor = 0.0f;
        float _timeScale = 1.0f;
        float _lastTime = -1.0f;
        float _elapsed = 0.0f;
        List<ProcessObject> _animatedObjects = new List<ProcessObject>();
        List<ProcessObject> _tickedObjects = new List<ProcessObject>();

        float _platformTime = 0.0f;

        bool _duringAdvance = false;
        bool _needPurgeEmpty = false;

        SimplePriorityQueue<IPrioritizable> _thinkHeap = new SimplePriorityQueue<IPrioritizable>(1024); // SimplePriorityQueue(1024);

        /// <summary>
        /// The scale at which time advances. If this is set to 2, the game         
        /// will play twice as fast. A value of 0.5 will run the         
        /// game at half speed. A value of 1 is normal.
        /// </summary>
        public float TimeScale
        {
            get
            {
                return _timeScale;
            }
            set
            {
                _timeScale = value;
            }
        }

        /// <summary>
        /// Used to determine how far we are between ticks. 0.0 at the start of a tick, and         
        /// 1.0 at the end. Useful for smoothly interpolating visual elements.
        /// </summary>
        public float InterpolationFactor
        {
            get { return _interpolationFactor; }
        }

        /// <summary>
        /// The amount of time that has been processed by the process manager. This does         
        /// take the time scale into account. Time is in milliseconds.
        /// </summary>
        public float VirtualTime
        {
            get { return _virtualTime; }
        }

        /// <summary>
        /// Current time reported by GameTime, updated every frame. Use this to get         
        /// GameTime, or if you want a unique number representing the current frame.
        /// </summary>
        public float PlatformTime
        {
            get { return _platformTime; }
        }

        /// <summary>
        /// Starts the process manager. This is automatically called when the first object         
        /// is added to the process manager. If the manager is stopped manually, then this         
        /// will have to be called to restart it.
        /// </summary>
        public void Start()
        {
            if (_started)
            {
                Logger.Warn(this, "Start", "The ProcessManager is already started.");
                return;
            }

            _lastTime = -1.0f;
            _elapsed = 0.0f;
            _started = true;
        }

        /// <summary>
        /// Stops the process manager. This is automatically called when the last object         
        /// is removed from the process manager, but can also be called manually to, for         
        /// example, pause the game.
        /// </summary>
        public void Stop()
        {
            if (!_started)
            {
                Logger.Warn(this, "Stop", "The ProcessManager isn't started.");
                return;
            }

            _started = false;
        }

        /// <summary>
        /// Returns true if the process manager is advancing.
        /// </summary>
        public bool IsTicking
        {
            get { return _started; }
        }

        /// <summary>
        /// Registers an object to receive frame callbacks.
        /// </summary>
        /// <param name="animatedObject">The object to add.</param>
        /// <param name="priority">
        /// The priority of the object. Objects added with higher priorities         
        /// will receive their callback before objects with lower priorities. The highest         
        /// (first-processed) priority is float.MaxValue. The lowest (last-processed)          
        /// priority is float.MinValue.
        /// </param>
#if WINDOWS_PHONE || XBOX
        public void AddAnimatedObject(IAnimatedObject animatedObject)
        {
            AddAnimatedObject(animatedObject, 0f);
        }

        public void AddAnimatedObject(IAnimatedObject animatedObject, float priority)
#else
        public void AddAnimatedObject(IAnimatedObject animatedObject, float priority = 0f)
#endif
        {
            AddObject(animatedObject, priority, _animatedObjects);
        }

        /// <summary>
        /// Unregisters an object from receiving frame callbacks.
        /// </summary>
        /// <param name="animatedObject">The object to remove.</param>
        public void RemoveAnimatedObject(IAnimatedObject animatedObject)
        {
            RemoveObject(animatedObject, _animatedObjects);
        }

        /// <summary>
        /// Registers an object to receive tick callbacks.
        /// </summary>
        /// <param name="tickedObject">The object to add.</param>
        /// <param name="priority">
        /// The priority of the object. Objects added with higher priorities         
        /// will receive their callback before objects with lower priorities. The highest         
        /// (first-processed) priority is float.MaxValue. The lowest (last-processed)          
        /// priority is float.MinValue.
        /// </param>
#if WINDOWS_PHONE || XBOX
        public void AddTickedObject(ITickedObject tickedObject)
        {
            AddTickedObject(tickedObject, 0f);
        }

        public void AddTickedObject(ITickedObject tickedObject, float priority)
#else
        public void AddTickedObject(ITickedObject tickedObject, float priority = 0f)
#endif
        {
            AddObject(tickedObject, priority, _tickedObjects);
        }

        /// <summary>
        /// Unregisters an object from receiving tick callbacks.
        /// </summary>
        /// <param name="tickedObject">The object to remove.</param>
        public void RemoveTickedObject(ITickedObject tickedObject)
        {
            RemoveObject(tickedObject, _tickedObjects);
        }

        /// <summary>
        /// Schedules a function to be called at a specified time in the future.
        /// </summary>
        /// <param name="delay">The number of milliseconds in the future to call the function.</param>
        /// <param name="callback">The function to call.</param>
        public void Schedule(float delay, Action callback)
        {
            if (!_started)
            {
                Start();
            }

            var schedule = new ScheduleObject();
            schedule.DueTime = _virtualTime + delay;
            schedule.Callback = callback;

            _thinkHeap.Enqueue(schedule);
        }

        /// <summary>
        /// Queue an IQueuedObject for callback. This is a very cheap way to have a callback         
        /// happen on an object. If an object is queued when it is already in the queue, it         
        /// is removed, then added.
        /// </summary>
        /// <param name="queuedObject">The object to add.</param>
        public void QueueObject(IQueuedObject queuedObject)
        {
            // Assert if this is in the past.
            if (queuedObject.NextThinkTime < _virtualTime)
            {
                throw new AAException("Tried to queue something into the past, but no flux capacitor is present!");
            }

            Profiler.Enter("queueObject");

            if (queuedObject.NextThinkTime >= _virtualTime && _thinkHeap.Contains(queuedObject))
            {
                _thinkHeap.Remove(queuedObject);
            }

            _thinkHeap.Enqueue(queuedObject);

            Profiler.Exit("queueObject");
        }

        /// <summary>
        /// Deferred function callback - called back at start of processing for next frame. Useful         
        /// any time you are going to do setTimeout(someFunc, 1) - it's a lot cheaper to do it          
        /// this way.
        /// </summary>
        /// <param name="method">Function to call.</param>
        public void CallLater(Action method)
        {
            _deferredMethodQueue.Add(method);
        }

        /// <summary>
        /// How many objects are depending on the ProcessManager right now?
        /// </summary>
        int ListenerCount
        {
            get { return _tickedObjects.Count + _animatedObjects.Count; }
        }

        /// <summary>
        /// Internal function add an object to a list with a given priority.
        /// </summary>
        /// <param name="obj">Object to add.</param>
        /// <param name="priority">Priority; this is used to keep the list ordered.</param>
        /// <param name="list">List to add to.</param>
        void AddObject(object obj, float priority, List<ProcessObject> list)
        {
            // If we are in a tick, defer the add.
            if (_duringAdvance)
            {
                CallLater(() => { AddObject(obj, priority, list); });
                return;
            }

            if (!_started)
            {
                Start();
            }

            var position = -1;
            for (var i = 0; i < list.Count; i++)
            {
                if (list[i] == null)
                {
                    continue;
                }

                if (list[i].Listener == obj)
                {
                    Logger.Warn(obj, "AddProcessObject", "This object has already been added to the process manager.");
                    return;
                }

                if (list[i].Priority < priority)
                {
                    position = i;
                    break;
                }
            }

            var processObject = new ProcessObject();
            processObject.Listener = obj;
            processObject.Priority = priority;
            processObject.ProfilerKey = obj.GetType().Name; //TypeUtility.getObjectClassName(obj);

            if (position < 0 || position >= list.Count)
            {
                list.Add(processObject);
            }
            else
            {
                list.Insert(position, processObject);
            }
        }

        /// <summary>
        /// Peer to addObject; removes an object from a list. 
        /// </summary>
        /// <param name="obj">Object to remove.</param>
        /// <param name="list">List from which to remove.</param>
        void RemoveObject(object obj, List<ProcessObject> list)
        {
            if (ListenerCount == 1 && _thinkHeap.Size == 0)
            {
                Stop();
            }

            for (var i = 0; i < list.Count; i++)
            {
                if (list[i] == null)
                {
                    continue;
                }

                if (list[i].Listener == obj)
                {
                    if (_duringAdvance)
                    {
                        list[i] = null;
                        _needPurgeEmpty = true;
                    }
                    else
                    {
                        list.RemoveAt(i);
                    }
                    return;
                }
            }

            Logger.Warn(obj, "RemoveProcessObject", "This object has not been added to the process manager.");
        }

        /// <summary>
        /// Main callback; this is called every frame and allows game logic to run.
        /// </summary>
        /// <param name="gameTime">The amount of game time that has elapsed.</param>
        public void Update(float gameTime)
        {
            // This is called from a system event, so it had better be at the 
            // root of the profiler stack!
            Profiler.EnsureAtRoot();

            // Track current time.
            float currentTime = gameTime;
            if (_lastTime < 0)
            {
                _lastTime = currentTime;
                return;
            }

            // Calculate time since last frame and advance that much.
            float deltaTime = (currentTime - _lastTime) * _timeScale;
            Advance(deltaTime);

            // Note new last time.
            _lastTime = currentTime;
        }

#if WINDOWS_PHONE || XBOX
        public void Advance(float deltaTime)
        {
            Advance(deltaTime, false);
        }

        public void Advance(float deltaTime, bool suppressSafety)
#else
        public void Advance(float deltaTime, bool suppressSafety = false)
#endif
        {
            _platformTime = deltaTime;

            // Note virtual time we started advancing from.
            float startTime = _virtualTime;

            // Add time to the accumulator.
            _elapsed += deltaTime;

            // Perform ticks, respecting tick caps.
            int tickCount = 0;
            while (_elapsed >= TICK_RATE_MS && (suppressSafety || tickCount < MAX_TICKS_PER_FRAME))
            {
                // Ticks always happen on interpolation boundary.
                _interpolationFactor = 0.0f;

                // Process pending events at this tick.                
                // This is done in the loop to ensure the correct order of events.
                ProcessScheduledObjects();

                // Do the onTick callbacks, noting time in profiler appropriately.
                Profiler.Enter("Tick");

                _duringAdvance = true;

                for (var j = 0; j < _tickedObjects.Count; j++)
                {
                    var obj = _tickedObjects[j];

                    if (obj == null)
                    {
                        continue;
                    }

                    Profiler.Enter(obj.ProfilerKey);
                    (obj.Listener as ITickedObject).OnTick(TICK_RATE);
                    Profiler.Exit(obj.ProfilerKey);
                }
                _duringAdvance = false;

                Profiler.Exit("Tick");

                // Update virtual time by subtracting from accumulator.
                _virtualTime += TICK_RATE_MS;
                _elapsed -= TICK_RATE_MS;
                tickCount++;
            }

            // Safety net - don't do more than a few ticks per frame to avoid death spirals.
            if (tickCount >= MAX_TICKS_PER_FRAME && !suppressSafety && !_disableSlowWarning)
            {
                // By default, only show when profiling.
                Logger.Warn(this, "Advance", "Exceeded maximum number of ticks for frame(" + _elapsed + "ms dropped).");
                _elapsed = 0;
            }

            // Make sure that we don't fall behind too far. This helps correct            
            // for short-term drops in framerate as well as the scenario where            
            // we are consistently running behind.
            _elapsed = MathHelper.Clamp(_elapsed, 0, 300);

            // Make sure we don't lose time to accumulation error.            
            // Not sure this gains us anything, so disabling -- BJG            
            //_virtualTime = startTime + deltaTime;

            // We process scheduled items again after tick processing to ensure between-tick schedules are hit            
            // Commenting this out because it can cause too-often calling of CallLater methods. -- BJG            
            // ProcessScheduledObjects();

            // Update objects wanting OnFrame callbacks.
            Profiler.Enter("Frame");
            _duringAdvance = true;
            _interpolationFactor = _elapsed / TICK_RATE_MS;
            for (var i = 0; i < _animatedObjects.Count; i++)
            {
                var obj = _animatedObjects[i];
                if (obj == null)
                {
                    continue;
                }

                Profiler.Enter(obj.ProfilerKey);
                (obj.Listener as IAnimatedObject).OnFrame(deltaTime / 1000);
                Profiler.Exit(obj.ProfilerKey);
            }
            _duringAdvance = false;
            Profiler.Exit("Frame");

            // Purge the lists if needed.            
            if(_needPurgeEmpty)           
            {                
                _needPurgeEmpty = false;          
                Profiler.Enter("purgeEmpty");
                for (var j = _animatedObjects.Count - 1; j >= 0; j--)     
                {
                    if (_animatedObjects[j] != null)
                    {
                        continue;
                    }

                    _animatedObjects.RemoveAt(j);  
                }
                for(var k = _tickedObjects.Count - 1; k >= 0 ; k--) 
                {
                    if (_tickedObjects[k] != null)
                    {
                        continue;
                    }

                    _tickedObjects.RemoveAt(k);  
                }
                Profiler.Exit("purgeEmpty"); 
            }

            Profiler.EnsureAtRoot();
        }

        private void ProcessScheduledObjects()
        {
            // Do any deferred methods.
            var oldDeferredMethodQueue = _deferredMethodQueue;
            if (oldDeferredMethodQueue.Count > 0)
            {
                Profiler.Enter("CallLater");

                // Put a new array in the queue to avoid getting into corrupted                
                // state due to more calls being added.
                _deferredMethodQueue = new List<Action>();

                for (var j = 0; j < oldDeferredMethodQueue.Count; j++)
                {
                    var currentDeferredMethod = oldDeferredMethodQueue[j];
                    currentDeferredMethod();
                }

                // Wipe the old array now we're done with it.
                oldDeferredMethodQueue.Clear();

                Profiler.Exit("CallLater");
            }

            // Process any queued items.
            if (_thinkHeap.Count > 0)
            {
                Profiler.Enter("Queue");

                while (_thinkHeap.Front != null && _thinkHeap.Front.Priority >= -_virtualTime)
                {
                    var item = _thinkHeap.Dequeue();
                    var queuedObject = item as IQueuedObject;
                    var scheduledObject = item as ScheduleObject;

                    Profiler.Enter(item.GetType().Name);

                    if (queuedObject != null)
                    {
                        // Check here to avoid else block that throws an error - empty callback
                        // means it unregistered.
                        if (queuedObject.NextThinkCallback != null)
                        {
                            queuedObject.NextThinkCallback();
                        }
                    }
                    else if (scheduledObject != null && scheduledObject.Callback != null)
                    {
                        scheduledObject.Callback();
                    }
                    else
                    {
                        throw new AAException("Unknown type found in the thinkHeap");
                    }

                    Profiler.Exit(item.GetType().Name);
                }

                Profiler.Exit("Queue");
            }
        }
    }
}
